﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using YasharEl.Infrastructure.ExceptionHandling;
using System.Globalization;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.Collections.Specialized;

namespace YasharEl.Infrastructure.ExceptionHandling.ServiceModel.FaultContacts
{
    public class FaultContractExceptionHandler : IExceptionHandler
    {
        #region Fields

        private Type faultContractType;
        private string exceptionMessage;
        private string exceptionMessageResourceKey;
        private IMessageTranslator messageTranslator;
        private NameValueCollection attributes;

        #endregion

        #region Constructors

        public FaultContractExceptionHandler(
            Type faultContractType,
            string exceptionMessage,
            string exceptionMessageResourceKey,
            IMessageTranslator messageTranslator,
            NameValueCollection attributes)
        {
            Contract.Requires<ArgumentNullException>(faultContractType != null, "faultContractType could not be null.");
            Contract.Requires<ArgumentNullException>(messageTranslator != null, "messageTranslator could not be null.");

            this.faultContractType = faultContractType;
            this.exceptionMessage = exceptionMessage;
            this.exceptionMessageResourceKey = exceptionMessageResourceKey;
            this.messageTranslator = messageTranslator;
            this.attributes = attributes;
        }

        #endregion

        #region IExceptionHandler Implementation

        public Exception HandleException(Exception exception, Guid handlingInstanceId)
        {
            EnsureDefaultConstructor();
            object fault = Activator.CreateInstance(faultContractType);
            PopulateFaultContractFromException(fault, exception, handlingInstanceId);

            string translatedMessage = exceptionMessage;
            if (!string.IsNullOrEmpty(exceptionMessageResourceKey))
            {
                translatedMessage = messageTranslator.Translate(exceptionMessageResourceKey);
            }

            return new FaultContractWrapperException(
                fault, handlingInstanceId, ExceptionUtility.GetMessage(exception, translatedMessage, handlingInstanceId));
        }

        #endregion

        #region Private Methods

        private void EnsureDefaultConstructor()
        {
            if (faultContractType.GetConstructor(Type.EmptyTypes) == null)
            {
                throw new MissingMethodException(
                    string.Format(CultureInfo.CurrentCulture,
                    " No parameterless constructor defined for the fault contract '{0}'. Define a default constructor for this contract.", faultContractType.FullName));
            }
        }

        private void PopulateFaultContractFromException(object fault, Exception exception, Guid handlingInstanceId)
        {
            if (this.attributes != null)
            {
                foreach (PropertyInfo property in this.faultContractType.GetProperties())
                {
                    if (PropertyIsMappedInAttributes(property))
                    {
                        // Try to map with  a configured property first
                        string configProperty = this.attributes[property.Name];
                        if (!string.IsNullOrEmpty(configProperty))
                        {
                            // strip out the {}
                            configProperty = configProperty.Replace("{", "").Replace("}", "");

                            if (PropertyIsGuid(property, configProperty))
                            {
                                property.SetValue(fault, handlingInstanceId, null);
                            }
                            else
                            {
                                PropertyInfo mappedExceptionProperty = GetMappedProperty(exception, configProperty);
                                if (PropertiesMatch(property, mappedExceptionProperty))
                                {
                                    // by now it's certain that properties have matching types
                                    // and values can be both read and written as required
                                    property.SetValue(fault,
                                        mappedExceptionProperty.GetValue(exception, null),
                                        null);
                                }
                            }
                        }
                    }
                    else
                    {
                        if (PropertyNamesMatch(property, exception))
                        {
                            // by now it's certain that properties have matching types
                            // and values can be both read and written as required
                            property.SetValue(fault,
                                GetExceptionProperty(property, exception).GetValue(exception, null),
                                null);
                        }
                    }
                }
            }
        }

        private bool PropertyIsGuid(PropertyInfo property, string configPropertyName)
        {
            return
                configPropertyName.Equals(
                    ExceptionShielding.HandlingInstanceIdPropertyMappingName,
                    StringComparison.InvariantCultureIgnoreCase)
                && property.PropertyType == typeof(Guid);
        }

        private bool PropertiesMatch(PropertyInfo faultProperty, PropertyInfo exceptionProperty)
        {
            // matching only occurs if properties have the same type, are not indexers,
            // and the fault contract property can be written while the exception property can be read
            return exceptionProperty != null
                && exceptionProperty.PropertyType == faultProperty.PropertyType
                && faultProperty.CanWrite
                && faultProperty.GetIndexParameters().Length == 0
                && exceptionProperty.CanRead
                && exceptionProperty.GetIndexParameters().Length == 0;
        }

        private bool PropertyNamesMatch(PropertyInfo faultProperty, Exception exception)
        {
            PropertyInfo exceptionProperty = GetExceptionProperty(faultProperty, exception);

            return PropertiesMatch(faultProperty, exceptionProperty);
        }

        private bool PropertyIsMappedInAttributes(PropertyInfo property)
        {
            return Array.Exists(this.attributes.AllKeys,
                delegate(string s) { return s == property.Name; });
        }

        private PropertyInfo GetMappedProperty(Exception exception, string configProperty)
        {
            return exception.GetType().GetProperty(configProperty);
        }

        private PropertyInfo GetExceptionProperty(PropertyInfo property, Exception exception)
        {
            return exception.GetType().GetProperty(property.Name);
        }

        #endregion
    }
}
